/*
 * Copyright (c) 2010-2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.pivot.feraldruid;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;

import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.util.Filter;

import org.apache.pivot.beans.BXML;
import org.apache.pivot.beans.BXMLSerializer;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.Map;
import org.apache.pivot.json.JSON;

import org.apache.pivot.wtk.Component;

import org.apache.pivot.wtk.ListButton;
import org.apache.pivot.wtk.ListButtonSelectionListener;

import org.apache.pivot.wtk.Slider;
import org.apache.pivot.wtk.SliderValueListener;
import org.apache.pivot.wtk.Label;
import org.apache.pivot.wtk.TableView;
import org.apache.pivot.wtk.TextInput;
import org.apache.pivot.wtk.content.NumericSpinnerData;

import org.apache.pivot.wtk.Border;
import org.apache.pivot.wtk.BoxPane;
import org.apache.pivot.wtk.Checkbox;
import org.apache.pivot.wtk.FileBrowserSheet;
import org.apache.pivot.wtk.PushButton;
import org.apache.pivot.wtk.Button;
import org.apache.pivot.wtk.ButtonPressListener;
import org.apache.pivot.wtk.Sheet;
import org.apache.pivot.wtk.SheetCloseListener;
import org.apache.pivot.wtk.Spinner;
import org.apache.pivot.wtk.TextInputContentListener;

import yawning.mew.pivot.MewDoubleRangeValidator;
import yawning.mew.pivot.MewDoubleValidator;
import yawning.mew.pivot.MewIntRangeValidator;
import yawning.mew.pivot.MewIntValidator;
import yawning.mew.pivot.MewPivot;
import yawning.mew.pivot.MewNumericInputListener;
import yawning.mew.pivot.MewParse;
import yawning.mew.pivot.MewTableViewMouseButtonListener;

/**
 * Mew Pivot Feral Druid specific UI components.
 *
 * TODO: The Simulator needs a better rotation editor UI.
 *
 * @author Yawning <yawninglol at gmail.com>
 */
public class Model {
	private Component mComponent;

	@BXML private ListButton modelButton;
	@BXML private Component simulatorPane;

	@BXML private Slider encounterDuration;
	@BXML private Slider bloodInTheWaterTime;
	@BXML private TextInput encounterDurationLabel;
	@BXML private TextInput targetCastInterval;
	@BXML private TextInput targetCastTime;
	@BXML private Checkbox generateRsv;
	@BXML private TextInput rsvStatScale;

	@BXML private Border catPane;
	@BXML private Spinner catTargetLevel;
	@BXML private TextInput targetArmor;
	@BXML private TextInput targetResilience;
	@BXML private TextInput bloodInTheWaterTimeLabel;

	@BXML private Border bearPane;
	@BXML private Spinner targetLevel;
	@BXML private TextInput targetSwingInterval;
	@BXML private TextInput targetSwingDamage;
	@BXML private BoxPane targetYellow;
	@BXML private TextInput targetYellowInterval;
	@BXML private TextInput targetYellowDamage;
	@BXML private BoxPane targetMagic;
	@BXML private TextInput targetMagicInterval;
	@BXML private TextInput targetMagicDamage;

	@BXML private TableView blizzardBugProps;

	@BXML private Border tangedynPane;
	@BXML private TextInput tangedynPulverizeUptime;
	@BXML private TextInput tangedynIncHPS;
	@BXML private TableView tangedynProps;

	@BXML private TextInput simulatorIterations;
	@BXML private TextInput simulatorThreads;
	@BXML private ListButton rngButton;
	@BXML private TextInput simulatorDurationJitter;

	@BXML private Label simulatorStrategyLabel;
	@BXML private PushButton simulatorStrategyLoad;
	@BXML private PushButton simulatorStrategyClear;

	@BXML private TextInput simulatorStrategyArgs;

	@BXML private BoxPane simulatorCatPane;
	@BXML private TableView simulatorCatProps;
	@BXML private Checkbox aggressiveRavage;
	@BXML private TextInput feralChargeTravelTime;
	@BXML private Label csvFileLabel;
	@BXML private PushButton csvFileSet;
	@BXML private PushButton csvFileClear;

	@BXML private BoxPane simulatorBearPane;
	@BXML private TableView simulatorBearProps;

	private File simulatorStrategy;
	private Filter<File> mDotScriptFilter;

	private File csvFile;
	private Filter<File> mDotCSVFilter;

	public Model() {
		BXMLSerializer bxmlSerializer = new BXMLSerializer();

		try {
			mComponent = (Component) bxmlSerializer.readObject(getClass().getResource("Model.bxml"));
		} catch (IOException e) {
			e.printStackTrace();
		} catch (SerializationException e) {
			e.printStackTrace();
		}

		bxmlSerializer.bind(this);

		// Setup the model selection button, and put the panes in a sane state.
		modelButton.getListButtonSelectionListeners().add(new ListButtonSelectionListener.Adapter() {
			@Override
			public void selectedIndexChanged(ListButton listButton, int prevIndex) {
				int index = listButton.getSelectedIndex();

				if (index != -1) {
					String item = (String) listButton.getListData().get(index);

					if (item.equals("Tangedyn Formulation (Bear)") == true) {
						catPane.setVisible(false);
						bearPane.setVisible(true);
						targetYellow.setVisible(false);
						targetMagic.setVisible(false);

						tangedynPane.setVisible(true);

						simulatorPane.setVisible(false);
						simulatorCatPane.setVisible(false);
						simulatorBearPane.setVisible(false);
					} else if (item.equals("Simulator (Cat)")) {
						catPane.setVisible(true);
						bearPane.setVisible(false);

						tangedynPane.setVisible(false);

						simulatorPane.setVisible(true);
						simulatorCatPane.setVisible(true);
						simulatorBearPane.setVisible(false);
					} else if (item.equals("Simulator (Bear)")) {
						catPane.setVisible(false);
						bearPane.setVisible(true);
						targetYellow.setVisible(true);
						targetMagic.setVisible(true);

						tangedynPane.setVisible(false);

						simulatorPane.setVisible(true);
						simulatorCatPane.setVisible(false);
						simulatorBearPane.setVisible(true);
					} else {
						throw new RuntimeException();
					}
				}
			}
		});
		modelButton.setSelectedItem("Simulator (Cat)");

		// Setup the encounter duration
		encounterDurationLabel.setValidator(new MewIntRangeValidator(1, 900));
		encounterDurationLabel.setStrictValidation(true);
		encounterDurationLabel.getTextInputContentListeners().add(new TextInputContentListener.Adapter() {
			@Override
			public void textChanged(TextInput textInput) {
				if (textInput.isTextValid() && textInput.getText().length() > 0) {
					encounterDuration.setValue(MewParse.parseInt(textInput.getText()));
				}
			}
		});
		encounterDuration.getSliderValueListeners().add(new SliderValueListener() {
			@Override
			public void valueChanged(Slider slider, int previousValue) {
				updateEncounterDuration();
			}
		});
		updateEncounterDuration();

		targetCastInterval.setValidator(new MewDoubleValidator());
		targetCastInterval.setStrictValidation(true);
		targetCastTime.setValidator(new MewDoubleValidator());
		targetCastTime.setStrictValidation(true);

		rsvStatScale.setValidator(new MewIntRangeValidator(0, 1000));
		rsvStatScale.setStrictValidation(true);

		// Setup common cat properties
		catTargetLevel.setSelectedIndex(((NumericSpinnerData) catTargetLevel.getSpinnerData()).indexOf(88));
		targetArmor.setValidator(new MewIntValidator());
		targetArmor.setStrictValidation(true);
		targetResilience.setValidator(new MewIntValidator());
		targetResilience.setStrictValidation(true);
		bloodInTheWaterTimeLabel.setValidator(new MewIntRangeValidator(0, 100));
		bloodInTheWaterTimeLabel.setStrictValidation(true);
		bloodInTheWaterTimeLabel.getTextInputContentListeners().add(new TextInputContentListener.Adapter() {
			@Override
			public void textChanged(TextInput textInput) {
				if (textInput.isTextValid()) {
					if (textInput.isTextValid() && textInput.getText().length() > 0) {
						bloodInTheWaterTime.setValue(MewParse.parseInt(textInput.getText()));
					}
				}
			}
		});
		bloodInTheWaterTime.getSliderValueListeners().add(new SliderValueListener() {
			@Override
			public void valueChanged(Slider slider, int previousValue) {
				updateBloodInTheWaterTime();
			}
		});
		updateBloodInTheWaterTime();

		// Setup common bear properties
		targetLevel.setSelectedIndex(((NumericSpinnerData) targetLevel.getSpinnerData()).indexOf(88));
		targetSwingInterval.setValidator(new MewDoubleValidator());
		targetSwingInterval.setStrictValidation(true);
		targetSwingDamage.setValidator(new MewIntValidator());
		targetSwingDamage.setStrictValidation(true);
		targetYellowInterval.setValidator(new MewDoubleValidator());
		targetYellowInterval.setStrictValidation(true);
		targetYellowDamage.setValidator(new MewIntValidator());
		targetYellowDamage.setStrictValidation(true);
		targetMagicInterval.setValidator(new MewDoubleValidator());
		targetMagicInterval.setStrictValidation(true);
		targetMagicDamage.setValidator(new MewIntValidator());
		targetMagicDamage.setStrictValidation(true);

		blizzardBugProps.getComponentMouseButtonListeners().add(new MewTableViewMouseButtonListener());

		tangedynProps.getComponentMouseButtonListeners().add(new MewTableViewMouseButtonListener());
		tangedynPulverizeUptime.setValidator(new MewDoubleRangeValidator(0.0, 1.0));
		tangedynPulverizeUptime.setStrictValidation(true);
		tangedynIncHPS.setValidator(new MewIntValidator());
		tangedynIncHPS.setStrictValidation(true);

		simulatorIterations.setValidator(new MewIntRangeValidator(1, 1000000));
		simulatorIterations.setStrictValidation(true);
		simulatorThreads.setValidator(new MewIntRangeValidator(0, 64));
		simulatorThreads.setStrictValidation(true);
		rngButton.setSelectedItem("WELL19937c");
		simulatorDurationJitter.setValidator(new MewIntRangeValidator(0, 100));
		simulatorDurationJitter.setStrictValidation(true);

		feralChargeTravelTime.setValidator(new MewIntRangeValidator(0,600));
		feralChargeTravelTime.setStrictValidation(true);
		simulatorCatProps.getComponentMouseButtonListeners().add(new MewTableViewMouseButtonListener());

		simulatorBearProps.getComponentMouseButtonListeners().add(new MewTableViewMouseButtonListener());

		mDotScriptFilter = new Filter<File>() {
			@Override
			public boolean include(File file) {
				return (file.isFile() && !file.getName().endsWith(".script"));
			}
		};
		simulatorStrategyLoad.getButtonPressListeners().add(new ButtonPressListener() {
			@Override
			public void buttonPressed(Button button) {
				FileBrowserSheet fileBrowserSheet = new FileBrowserSheet(FileBrowserSheet.Mode.OPEN);
				fileBrowserSheet.setRootDirectory(MewPivot.getWorkingDirectory());
				fileBrowserSheet.setDisabledFileFilter(mDotScriptFilter);
				fileBrowserSheet.getStyles().put("hideDisabledFiles", true);
				fileBrowserSheet.open(button.getWindow(), new SheetCloseListener() {
					@Override
					public void sheetClosed(Sheet sheet) {
						if (sheet.getResult()) {
							simulatorStrategy = ((FileBrowserSheet) sheet).getSelectedFile();
							MewPivot.setWorkingDirectory(simulatorStrategy);
							updateSimulatorStrategy();
						}
					}
				});
			}
		});
		simulatorStrategyClear.getButtonPressListeners().add(new ButtonPressListener() {
			@Override
			public void buttonPressed(Button button) {
				simulatorStrategy = null;
				updateSimulatorStrategy();
			}
		});
		updateSimulatorStrategy();

		mDotCSVFilter = new Filter<File>() {
			@Override
			public boolean include(File file) {
				return (file.isFile() && !file.getName().endsWith(".csv"));
			}
		};

		csvFileSet.getButtonPressListeners().add(new ButtonPressListener() {
			@Override
			public void buttonPressed(Button button) {
				FileBrowserSheet fileBrowserSheet = new FileBrowserSheet(FileBrowserSheet.Mode.SAVE_AS);
				fileBrowserSheet.setRootDirectory(MewPivot.getWorkingDirectory());
				fileBrowserSheet.setDisabledFileFilter(mDotCSVFilter);
				fileBrowserSheet.getStyles().put("hideDisabledFiles", true);
				fileBrowserSheet.open(button.getWindow(), new SheetCloseListener() {
					@Override
					public void sheetClosed(Sheet sheet) {
						if (sheet.getResult()) {
							csvFile = ((FileBrowserSheet) sheet).getSelectedFile();

							// Force append .csv to the file.
							if (csvFile.getName().endsWith(".csv") == false) {
								File newFile = new File(csvFile.getParent(), csvFile.getName() + ".csv");
								csvFile = newFile;
							}

							MewPivot.setWorkingDirectory(csvFile);
							updateCSVFile();
						}
					}
				});
			}
		});
		csvFileClear.getButtonPressListeners().add(new ButtonPressListener() {
			@Override
			public void buttonPressed(Button button) {
				csvFile = null;
				updateCSVFile();
			}
		});
		updateCSVFile();

		// Add listeners to fix up zero length text for inputs where we expect numbers.
		encounterDurationLabel.getComponentStateListeners().add(new MewNumericInputListener("1"));
		targetCastInterval.getComponentStateListeners().add(new MewNumericInputListener("0.0"));
		targetCastTime.getComponentStateListeners().add(new MewNumericInputListener("0.0"));
		rsvStatScale.getComponentStateListeners().add(new MewNumericInputListener());
		targetArmor.getComponentStateListeners().add(new MewNumericInputListener());
		targetResilience.getComponentStateListeners().add(new MewNumericInputListener());
		bloodInTheWaterTimeLabel.getComponentStateListeners().add(new MewNumericInputListener());
		targetSwingInterval.getComponentStateListeners().add(new MewNumericInputListener("0.0"));
		targetSwingDamage.getComponentStateListeners().add(new MewNumericInputListener());
		targetYellowInterval.getComponentStateListeners().add(new MewNumericInputListener("0.0"));
		targetYellowDamage.getComponentStateListeners().add(new MewNumericInputListener());
		targetMagicInterval.getComponentStateListeners().add(new MewNumericInputListener("0.0"));
		targetMagicDamage.getComponentStateListeners().add(new MewNumericInputListener());
		tangedynPulverizeUptime.getComponentStateListeners().add(new MewNumericInputListener("0.0"));
		tangedynIncHPS.getComponentStateListeners().add(new MewNumericInputListener());
		simulatorIterations.getComponentStateListeners().add(new MewNumericInputListener("1"));
		simulatorThreads.getComponentStateListeners().add(new MewNumericInputListener());
		simulatorDurationJitter.getComponentStateListeners().add(new MewNumericInputListener());
		feralChargeTravelTime.getComponentStateListeners().add(new MewNumericInputListener());
	}

	public Component getComponent() {
		return mComponent;
	}

	private void updateEncounterDuration() {
		if (!Integer.toString(encounterDuration.getValue()).equals(encounterDurationLabel.getText()))
			encounterDurationLabel.setText(Integer.toString(encounterDuration.getValue()));
	}

	private void updateBloodInTheWaterTime() {
		if (!Integer.toString(bloodInTheWaterTime.getValue()).equals(bloodInTheWaterTimeLabel.getText()))
			bloodInTheWaterTimeLabel.setText(Integer.toString(bloodInTheWaterTime.getValue()));
	}

	private void updateSimulatorStrategy() {
		if (simulatorStrategy != null) {
			simulatorStrategyLabel.setText(simulatorStrategy.getName());
			simulatorStrategyClear.setVisible(true);
		} else {
			simulatorStrategyLabel.setText(null);
			simulatorStrategyClear.setVisible(false);
		}
	}

	private void updateCSVFile() {
		if (csvFile != null) {
			csvFileLabel.setText(csvFile.getName());
			csvFileClear.setVisible(true);
		} else {
			csvFileLabel.setText(null);
			csvFileClear.setVisible(false);
		}
	}

	/**
	 * Read the strategy in from the specified file.
	 */
	private String readSimStrat(File fileName) {
		StringBuilder strat = new StringBuilder();

		try {
			BufferedReader reader = new BufferedReader(new FileReader(fileName));

			try {
				String line = null;
				while ((line = reader.readLine()) != null) {
					strat.append(line);
					strat.append("\n");
				}
			} finally {
				reader.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}

		return strat.toString();
	}

	@SuppressWarnings("unchecked")
	public Properties toProperties() {
		Properties rval = new Properties();

		boolean isCat = false;
		boolean isBear = false;
		boolean isSim = false;

		// Determine which model the user is interested in running.
		String model = (String) modelButton.getListData().get(modelButton.getSelectedIndex());
		if (model.equals("Toskk Formulation (Cat)") == true) {
			model = "yawning.mew.cat.CatModelToskk";
			isCat = true;
		} else if (model.equals("Simulator (Cat)")) {
			model = "yawning.mew.cat.CatModelSimulation";
			isCat = true;
			isSim = true;
		} else if (model.equals("Tangedyn Formulation (Bear)")) {
			model = "yawning.mew.bear.BearMitigationFormulation";
			isBear = true;
		} else if (model.equals("Simulator (Bear)")) {
			model = "yawning.mew.bear.BearThreatSimulation";
			isBear = true;
			isSim = true;
		} else {
			throw new RuntimeException();
		}
		rval.setProperty("Mew.Class", model);

		// Common properties
		rval.setProperty("Simulator.Duration", Integer.toString(encounterDuration.getValue()));
		rval.setProperty("Pivot.GenerateRsv", Boolean.toString(generateRsv.isSelected()));
		rval.setProperty("Pivot.StatScale", rsvStatScale.getText());
		rval.setProperty("Target.CastInterval", targetCastInterval.getText());
		rval.setProperty("Target.CastLength",targetCastTime.getText());

		// Common cat properties
		if (isCat) {
			rval.setProperty("Target.Level", Integer.toString(((NumericSpinnerData) catTargetLevel.getSpinnerData()).get(catTargetLevel.getSelectedIndex())));
			rval.setProperty("Target.Armor", targetArmor.getText());
			rval.setProperty("Target.Resilience", targetResilience.getText());
			rval.setProperty("BloodInTheWaterTime", Double.toString((double) bloodInTheWaterTime.getValue() / 100));
		}

		// Common bear properties
		if (isBear) {
			rval.setProperty("Target.Level", Integer.toString(((NumericSpinnerData) targetLevel.getSpinnerData()).get(targetLevel.getSelectedIndex())));
			rval.setProperty("Target.SwingInterval", targetSwingInterval.getText());
			rval.setProperty("Target.SwingDamage", targetSwingDamage.getText());
			rval.setProperty("Target.YellowInterval", targetYellowInterval.getText());
			rval.setProperty("Target.YellowDamage", targetYellowDamage.getText());
			rval.setProperty("Target.MagicInterval", targetMagicInterval.getText());
			rval.setProperty("Target.MagicDamage", targetMagicDamage.getText());
		}

		// Blizzard bugs
		for (Map<String,Object> row : (List<Map<String,Object>>) blizzardBugProps.getTableData()) {
			String key = JSON.get(row, "prop");
			boolean value = (Boolean) JSON.get(row, "enabled");

			rval.setProperty(key, Boolean.toString(value));
		}

		// Bear Formulation properties
		if (isBear && !isSim) {
			for (Map<String,Object> row : (List<Map<String,Object>>) tangedynProps.getTableData()) {
				String key = JSON.get(row, "prop");
				boolean value = (Boolean) JSON.get(row, "enabled");

				rval.setProperty(key, Boolean.toString(value));
			}

			rval.setProperty("BearMitigationFormulation.PulverizeUptime", tangedynPulverizeUptime.getText());
			rval.setProperty("BearMitigationFormulation.IncomingHPS", tangedynIncHPS.getText());
		}

		// Generic Simulation properties
		if (isSim) {
			rval.setProperty("Simulator.Iterations", simulatorIterations.getText());
			rval.setProperty("Simulator.Threads", simulatorThreads.getText());
			rval.setProperty("Simulator.DurationJitter", simulatorDurationJitter.getText());

			String prng = (String) rngButton.getListData().get(rngButton.getSelectedIndex());
			if (prng.equals("WELL19937c")) {
				rval.setProperty("Simulator.PRNG", "org.apache.commons.math.random.Well19937c");
			} else if (prng.equals("Mersenne Twister")) {
				rval.setProperty("Simulator.PRNG", "org.apache.commons.math.random.MersenneTwister");
			} else if (prng.equals("IBAA")) {
				rval.setProperty("Simulator.PRNG", "yawning.mew.util.IBAA");
			} else {
				throw new RuntimeException();
			}

			if (simulatorStrategy != null) {
				String strategy = readSimStrat(simulatorStrategy);
				if (strategy != null) {
					rval.setProperty("Simulator.Strategy", strategy);
				}
			}

			rval.setProperty("Simulator.StrategyArg", simulatorStrategyArgs.getText());

			// Cat specific Simulation properties
			if (isCat) {
				for (Map<String,Object> row : (List<Map<String,Object>>) simulatorCatProps.getTableData()) {
					String key = JSON.get(row, "prop");
					boolean value = (Boolean) JSON.get(row, "enabled");

					rval.setProperty(key, Boolean.toString(value));
				}
				rval.setProperty("Simulator.AggressiveFeralCharge", Boolean.toString(aggressiveRavage.isSelected()));
				rval.setProperty("Simulator.FeralChargeTravelTime", feralChargeTravelTime.getText());

				if (csvFile != null) {
					rval.setProperty("Simulator.LogDamage", "true");
					rval.setProperty("Pivot.CSVFile", csvFile.toString());
				}
			}

			// Bear specific Simulation properties
			if (isBear) {
				for (Map<String,Object> row : (List<Map<String,Object>>) simulatorBearProps.getTableData()) {
					String key = JSON.get(row, "prop");
					boolean value = (Boolean) JSON.get(row, "enabled");

					rval.setProperty(key, Boolean.toString(value));
				}

				rval.setProperty("Simulator.Vengeance", "true");
			}

			if (Boolean.parseBoolean(rval.getProperty("Pivot.ReseedPRNG", "false"))) {
				rval.setProperty("Simulator.Seed", Integer.toString((int)System.currentTimeMillis()));
			}
		}

		return rval;
	}

	@SuppressWarnings("unchecked")
	public void onLoad(Map<String,Object> backingStore) {
		Map<String,?> tmp;

		tmp = JSON.get(backingStore, "model");
		if (tmp != null) {
			if (tmp.containsKey("active")) {
				String activeModel = (String) tmp.get("active");

				// Backward compatibility.
				if (activeModel.equals("Simulator (Experimental)")) activeModel = "Simulator (Cat)";
				if (activeModel.equals("Simulator")) activeModel = "Simulator (Cat)";
				if (activeModel.equals("Toskk (Formulation)")) activeModel = "Toskk Formulation (Cat)";
				if (activeModel.equals("Toskk Formulation (Cat)")) activeModel = "Simulator (Cat)";

				modelButton.setSelectedItem(activeModel);
			}
			if (tmp.containsKey("encounterDuration")) { encounterDuration.setValue((Integer) tmp.get("encounterDuration")); }
			if (tmp.containsKey("targetCastInterval")) { targetCastInterval.setText(Double.toString((Double) tmp.get("targetCastInterval"))); }
			if (tmp.containsKey("targetCastTime")) { targetCastTime.setText(Double.toString((Double) tmp.get("targetCastTime"))); }
			if (tmp.containsKey("generateRsv")) { generateRsv.setSelected((Boolean) tmp.get("generateRsv")); }
			if (tmp.containsKey("rsvStatScale")) { rsvStatScale.setText(Integer.toString((Integer) tmp.get("rsvStatScale"))); }

			if (tmp.containsKey("catTargetLevel")) { catTargetLevel.setSelectedIndex(((NumericSpinnerData) catTargetLevel.getSpinnerData()).indexOf((Integer) tmp.get("catTargetLevel"))); }
			if (tmp.containsKey("targetArmor")) { targetArmor.setText(Integer.toString((Integer) tmp.get("targetArmor"))); }
			if (tmp.containsKey("targetResilience")) { targetResilience.setText(Integer.toString((Integer) tmp.get("targetResilience"))); }
			if (tmp.containsKey("bloodInTheWaterTime")) { bloodInTheWaterTime.setValue((Integer) tmp.get("bloodInTheWaterTime")); }

			if (tmp.containsKey("targetLevel")) { targetLevel.setSelectedIndex(((NumericSpinnerData) targetLevel.getSpinnerData()).indexOf((Integer) tmp.get("targetLevel"))); }
			if (tmp.containsKey("targetSwingInterval")) { targetSwingInterval.setText(Double.toString((Double) tmp.get("targetSwingInterval"))); }
			if (tmp.containsKey("targetSwingDamage")) { targetSwingDamage.setText(Integer.toString((Integer) tmp.get("targetSwingDamage"))); }
			if (tmp.containsKey("targetYellowInterval")) { targetYellowInterval.setText(Double.toString((Double) tmp.get("targetYellowInterval"))); }
			if (tmp.containsKey("targetYellowDamage")) { targetYellowDamage.setText(Integer.toString((Integer) tmp.get("targetYellowDamage"))); }
			if (tmp.containsKey("targetMagicInterval")) { targetMagicInterval.setText(Double.toString((Double) tmp.get("targetMagicInterval"))); }
			if (tmp.containsKey("targetMagicDamage")) { targetMagicDamage.setText(Integer.toString((Integer) tmp.get("targetMagicDamage"))); }
		}

		// Blizzard bugs
		tmp = JSON.get(backingStore, "model.blizzardBugProps");
		if (tmp != null) {
			for (Map<String,Object> row : (List<Map<String,Object>>) blizzardBugProps.getTableData()) {
				String key = JSON.get(row, "prop");

				if (tmp.containsKey(key)) {
					boolean enabled = (Boolean) tmp.get(key);
					JSON.put(row, "enabled", enabled);
				}
			}
		}

		// Bear Formulation
		tmp = JSON.get(backingStore, "model.tangedynProps");
		if (tmp != null) {
			for (Map<String,Object> row : (List<Map<String,Object>>) tangedynProps.getTableData()) {
				String key = JSON.get(row, "prop");

				if (tmp.containsKey(key)) {
					boolean enabled = (Boolean) tmp.get(key);
					JSON.put(row, "enabled", enabled);
				}
			}

			if (tmp.containsKey("pulverizeUptime")) {tangedynPulverizeUptime.setText(Double.toString((Double) tmp.get("pulverizeUptime"))); }
			if (tmp.containsKey("incomingHPS")) { tangedynIncHPS.setText(Integer.toString((Integer) tmp.get("incomingHPS"))); }
		}

		// Simulation
		tmp = JSON.get(backingStore, "model.simulatorProps");
		if (tmp != null) {
			if (tmp.containsKey("PRNG")) { rngButton.setSelectedItem((String) tmp.get("PRNG")); }
			if (tmp.containsKey("strategyArg")) { simulatorStrategyArgs.setText((String) tmp.get("strategyArg")); }
			if (tmp.containsKey("Simulator.Iterations")) { simulatorIterations.setText(Integer.toString((Integer) tmp.get("Simulator.Iterations"))); }
			if (tmp.containsKey("Simulator.Threads")) { simulatorThreads.setText(Integer.toString((Integer) tmp.get("Simulator.Threads"))); }
			if (tmp.containsKey("Simulator.DurationJitter")) { simulatorDurationJitter.setText(Integer.toString((Integer) tmp.get("Simulator.DurationJitter"))); }
			if (tmp.containsKey("Simulator.Strategy")) { simulatorStrategy = new File((String) tmp.get("Simulator.Strategy")); }
		}

		// Simulation (Bear)
		tmp = JSON.get(backingStore, "model.simulatorProps.bear");
		if (tmp != null) {
			for (Map<String,Object> row : (List<Map<String,Object>>) simulatorBearProps.getTableData()) {
				String key = JSON.get(row, "prop");

				if (tmp.containsKey(key)) {
					boolean enabled = (Boolean) tmp.get(key);
					JSON.put(row, "enabled", enabled);
				}
			}
		}

		// Simulation (Cat)
		tmp = JSON.get(backingStore, "model.simulatorProps.cat");
		if (tmp != null) {
			for (Map<String,Object> row : (List<Map<String,Object>>) simulatorCatProps.getTableData()) {
				String key = JSON.get(row, "prop");

				if (tmp.containsKey(key)) {
					boolean enabled = (Boolean) tmp.get(key);
					JSON.put(row, "enabled", enabled);
				}
			}

			if (tmp.containsKey("aggressiveRavage")) { aggressiveRavage.setSelected((Boolean) tmp.get("aggressiveRavage")); }
			if (tmp.containsKey("feralChargeTravelTime")) { feralChargeTravelTime.setText(Integer.toString((Integer) tmp.get("feralChargeTravelTime"))); }
			if (tmp.containsKey("CSVFile")) { csvFile = new File((String) tmp.get("CSVFile")); }
		}

		// Backwards compatibility
		tmp = JSON.get(backingStore, "model");
		if (tmp != null) {
			if (tmp.containsKey("aggressiveRavage")) { aggressiveRavage.setSelected((Boolean) tmp.get("aggressiveRavage")); }
			if (tmp.containsKey("feralChargeTravelTime")) { feralChargeTravelTime.setText(Integer.toString((Integer) tmp.get("feralChargeTravelTime"))); }
		}
		tmp = JSON.get(backingStore, "model.simulatorProps");
		if (tmp != null) {
			for (Map<String,Object> row : (List<Map<String,Object>>) simulatorCatProps.getTableData()) {
				String key = JSON.get(row, "prop");

				if (tmp.containsKey(key)) {
					boolean enabled = (Boolean) tmp.get(key);
					JSON.put(row, "enabled", enabled);
				}
			}
		}

		// Put things in a sane state.
		updateSimulatorStrategy();
		updateCSVFile();
		updateEncounterDuration();
		updateBloodInTheWaterTime();
	}

	@SuppressWarnings("unchecked")
	public void onSave(Map<String,Object> backingStore) {
		HashMap<String,Object> tmp;

		tmp = new HashMap<String,Object>();

		int index = modelButton.getSelectedIndex();
		if (index != -1) {
			String item = (String) modelButton.getListData().get(index);

			tmp.put("active", item);
		}
		tmp.put("encounterDuration", encounterDuration.getValue());
		tmp.put("targetCastInterval", MewParse.parseDouble(targetCastInterval.getText()));
		tmp.put("targetCastTime", MewParse.parseDouble(targetCastTime.getText()));
		tmp.put("generateRsv", generateRsv.isSelected());
		tmp.put("rsvStatScale", MewParse.parseInt(rsvStatScale.getText()));

		tmp.put("catTargetLevel", ((NumericSpinnerData) catTargetLevel.getSpinnerData()).get(catTargetLevel.getSelectedIndex()));
		tmp.put("targetArmor", MewParse.parseInt(targetArmor.getText()));
		tmp.put("targetResilience", MewParse.parseInt(targetResilience.getText()));
		tmp.put("bloodInTheWaterTime", bloodInTheWaterTime.getValue());

		tmp.put("targetLevel", ((NumericSpinnerData) targetLevel.getSpinnerData()).get(targetLevel.getSelectedIndex()));
		tmp.put("targetSwingInterval", MewParse.parseDouble(targetSwingInterval.getText()));
		tmp.put("targetSwingDamage", MewParse.parseInt(targetSwingDamage.getText()));
		tmp.put("targetYellowInterval", MewParse.parseDouble(targetYellowInterval.getText()));
		tmp.put("targetYellowDamage", MewParse.parseInt(targetYellowDamage.getText()));
		tmp.put("targetMagicInterval", MewParse.parseDouble(targetMagicInterval.getText()));
		tmp.put("targetMagicDamage", MewParse.parseInt(targetMagicDamage.getText()));

		JSON.put(backingStore, "model", tmp);

		// Blizzard bugs
		tmp = new HashMap<String,Object>();
		for (Map<String,Object> row : (List<Map<String,Object>>) blizzardBugProps.getTableData()) {
			String key = JSON.get(row, "prop");
			boolean value = (Boolean) JSON.get(row, "enabled");

			tmp.put(key, value);
		}
		JSON.put(backingStore, "model.blizzardBugProps", tmp);

		// Bear Formulation
		tmp = new HashMap<String,Object>();
		for (Map<String,Object> row : (List<Map<String,Object>>) tangedynProps.getTableData()) {
			String key = JSON.get(row, "prop");
			boolean value = (Boolean) JSON.get(row, "enabled");

			tmp.put(key, value);
		}
		tmp.put("pulverizeUptime", MewParse.parseDouble(tangedynPulverizeUptime.getText()));
		tmp.put("incomingHPS", MewParse.parseInt(tangedynIncHPS.getText()));
		JSON.put(backingStore, "model.tangedynProps", tmp);

		// Simulation
		tmp = new HashMap<String,Object>();
		tmp.put("Simulator.Iterations", MewParse.parseInt(simulatorIterations.getText()));
		tmp.put("Simulator.Threads", MewParse.parseInt(simulatorThreads.getText()));
		index = rngButton.getSelectedIndex();
		if (index != -1) {
			tmp.put("PRNG", (String) rngButton.getListData().get(index));
		}
		tmp.put("Simulator.DurationJitter", MewParse.parseInt(simulatorDurationJitter.getText()));
		if (simulatorStrategy != null) tmp.put("Simulator.Strategy", simulatorStrategy.toString());
		tmp.put("strategyArg", simulatorStrategyArgs.getText());
		JSON.put(backingStore, "model.simulatorProps", tmp);

		// Cat Simulation
		tmp = new HashMap<String,Object>();
		for (Map<String,Object> row : (List<Map<String,Object>>) simulatorCatProps.getTableData()) {
			String key = JSON.get(row, "prop");
			boolean value = (Boolean) JSON.get(row, "enabled");

			tmp.put(key, value);
		}
		tmp.put("aggressiveRavage", aggressiveRavage.isSelected());
		tmp.put("feralChargeTravelTime", MewParse.parseInt(feralChargeTravelTime.getText()));
		if (csvFile != null) tmp.put("CSVFile", csvFile.toString());
		JSON.put(backingStore, "model.simulatorProps.cat", tmp);

		// Bear Simulation
		tmp = new HashMap<String,Object>();
		for (Map<String,Object> row : (List<Map<String,Object>>) simulatorBearProps.getTableData()) {
			String key = JSON.get(row, "prop");
			boolean value = (Boolean) JSON.get(row, "enabled");

			tmp.put(key, value);
		}
		JSON.put(backingStore, "model.simulatorProps.bear", tmp);
	}
}
